Passed
Push — task/application-touch-step-st... ( 8f1a36 )
by Tristan
07:12
created

applicationHooks.tsx ➔ useTouchApplicationStep   A

Complexity

Conditions 2

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 19
rs 9.9
cc 2
1
/* eslint-disable camelcase */
2
import { useCallback, useEffect, useState } from "react";
3
import { useSelector } from "react-redux";
4
import { DispatchType } from "../configureStore";
5
import { RootState } from "../store/store";
6
import { getAwardRecipientTypes as fetchAwardRecipientTypes } from "../store/AwardRecipientType/awardRecipientTypeActions";
7
import { getAwardRecipientTypes } from "../store/AwardRecipientType/awardRecipientTypeSelector";
8
import { getAwardRecognitionTypes as fetchAwardRecognitionTypes } from "../store/AwardRecognitionType/awardRecognitionTypeActions";
9
import { getAwardRecognitionTypes } from "../store/AwardRecognitionType/awardRecognitionTypeSelector";
10
import { getEducationTypes as fetchEducationTypes } from "../store/EducationType/educationTypeActions";
11
import { getEducationTypes } from "../store/EducationType/educationTypeSelector";
12
import { getEducationStatuses as fetchEducationStatuses } from "../store/EducationStatus/educationStatusActions";
13
import { getEducationStatuses } from "../store/EducationStatus/educationStatusSelector";
14
import {
15
  AwardRecipientType,
16
  AwardRecognitionType,
17
  EducationType,
18
  EducationStatus,
19
  Job,
20
  Experience as ExperienceType,
21
  Skill,
22
  ExperienceSkill,
23
  ApplicationNormalized,
24
  Criteria,
25
  JobPosterQuestion,
26
  JobApplicationAnswer,
27
  Application,
28
  User,
29
} from "../models/types";
30
import {
31
  getApplicationById,
32
  getApplicationIsUpdating,
33
  getApplicationNormalized,
34
  getJobApplicationAnswers,
35
  getJobApplicationSteps,
36
} from "../store/Application/applicationSelector";
37
import {
38
  fetchApplication,
39
  touchApplicationStep,
40
} from "../store/Application/applicationActions";
41
import {
42
  getCriteriaByJob,
43
  getJob,
44
  getJobIsUpdating,
45
  getJobPosterQuestionsByJob,
46
} from "../store/Job/jobSelector";
47
import { fetchJob } from "../store/Job/jobActions";
48
import {
49
  ApplicationStatusId,
50
  ApplicationStep,
51
  ApplicationStepId,
52
  ProgressBarStatus,
53
} from "../models/lookupConstants";
54
import {
55
  getExperienceByApplicant,
56
  getExperienceByApplication,
57
  getExperienceSkillsByApplicant,
58
  getExperienceSkillsByApplication,
59
  getUpdatingByApplicant,
60
  getUpdatingByApplication,
61
} from "../store/Experience/experienceSelector";
62
import {
63
  fetchExperienceByApplicant,
64
  fetchExperienceByApplication,
65
} from "../store/Experience/experienceActions";
66
import { getSkills, getSkillsUpdating } from "../store/Skill/skillSelector";
67
import { fetchSkills } from "../store/Skill/skillActions";
68
import { getUserById } from "../store/User/userSelector";
69
import { fetchUser } from "../store/User/userActions";
70
71
export function useUser(userId: number | undefined): User | null {
72
  return useSelector((state: RootState) =>
73
    userId ? getUserById(state, { userId }) : null,
74
  );
75
}
76
export function useApplication(
77
  applicationId: number,
78
): ApplicationNormalized | null {
79
  return useSelector((state: RootState) =>
80
    getApplicationNormalized(state, { applicationId }),
81
  );
82
}
83
84
export function useReviewedApplication(
85
  applicationId: number,
86
): Application | null {
87
  return useSelector((state: RootState) =>
88
    getApplicationById(state, { id: applicationId }),
89
  );
90
}
91
92
export function useJob(jobId: number | undefined): Job | null {
93
  return useSelector((state: RootState) =>
94
    jobId ? getJob(state, { jobId }) : null,
95
  );
96
}
97
98
export function useExperienceConstants(): {
99
  awardRecipientTypes: AwardRecipientType[];
100
  awardRecognitionTypes: AwardRecognitionType[];
101
  educationTypes: EducationType[];
102
  educationStatuses: EducationStatus[];
103
} {
104
  const awardRecipientTypes = useSelector(getAwardRecipientTypes);
105
  const awardRecognitionTypes = useSelector(getAwardRecognitionTypes);
106
  const educationTypes = useSelector(getEducationTypes);
107
  const educationStatuses = useSelector(getEducationStatuses);
108
  return {
109
    awardRecipientTypes,
110
    awardRecognitionTypes,
111
    educationTypes,
112
    educationStatuses,
113
  };
114
}
115
116
export function useSkills(): Skill[] {
117
  return useSelector(getSkills);
118
}
119
120
export function useCriteria(jobId: number | undefined): Criteria[] {
121
  return useSelector((state: RootState) =>
122
    jobId ? getCriteriaByJob(state, { jobId }) : [],
123
  );
124
}
125
126
export function useExperiences(
127
  applicationId: number,
128
  application: ApplicationNormalized | null,
129
): ExperienceType[] {
130
  const applicantId = application?.applicant_id ?? 0;
131
132
  // When an Application is still a draft, use Experiences associated with the applicant profile.
133
  // When an Application has been submitted and is no longer a draft, display Experience associated with the Application directly.
134
  const useProfileExperience =
135
    application === null ||
136
    application.application_status_id === ApplicationStatusId.draft;
137
138
  // This selector must be memoized because getExperienceByApplicant/Application uses reselect, and not re-reselect, so it needs to preserve its state.
139
  const experienceSelector = useCallback(
140
    (state: RootState) =>
141
      useProfileExperience
142
        ? getExperienceByApplicant(state, { applicantId })
143
        : getExperienceByApplication(state, { applicationId }),
144
    [applicationId, applicantId, useProfileExperience],
145
  );
146
  const experiencesByType = useSelector(experienceSelector);
147
  const experiences: ExperienceType[] = [
148
    ...experiencesByType.award,
149
    ...experiencesByType.community,
150
    ...experiencesByType.education,
151
    ...experiencesByType.personal,
152
    ...experiencesByType.work,
153
  ];
154
  return experiences;
155
}
156
157
export function useExperienceSkills(
158
  applicationId: number,
159
  application: ApplicationNormalized | null,
160
): ExperienceSkill[] {
161
  // ExperienceSkills don't need to be fetched because they are returned in the Experiences API calls.
162
  const applicantId = application?.applicant_id ?? 0;
163
  const useProfileExperience =
164
    application === null ||
165
    application.application_status_id === ApplicationStatusId.draft;
166
  const expSkillSelector = (state: RootState) =>
167
    useProfileExperience
168
      ? getExperienceSkillsByApplicant(state, { applicantId })
169
      : getExperienceSkillsByApplication(state, { applicationId });
170
  const experienceSkills = useSelector(expSkillSelector);
171
  return experienceSkills;
172
}
173
174
export function useJobPosterQuestions(
175
  jobId: number | undefined,
176
): JobPosterQuestion[] {
177
  return useSelector((state: RootState) =>
178
    jobId ? getJobPosterQuestionsByJob(state, { jobId }) : [],
179
  );
180
}
181
182
export function useJobApplicationAnswers(
183
  applicationId: number,
184
): JobApplicationAnswer[] {
185
  return useSelector((state: RootState) =>
186
    getJobApplicationAnswers(state, { applicationId }),
187
  );
188
}
189
190
export function useApplicationUser(applicationId: number): User | null {
191
  const application = useApplication(applicationId);
192
  const user = application?.applicant?.user ?? null;
193
  return user;
194
}
195
196
/**
197
 * Return all skills from the redux store, and fetch the skills from backend if they are not yet in the store.
198
 * @param dispatch
199
 */
200
export function useFetchSkills(dispatch: DispatchType): Skill[] {
201
  const skills = useSelector(getSkills);
202
  const skillsUpdating = useSelector(getSkillsUpdating);
203
  useEffect(() => {
204
    if (skills.length === 0 && !skillsUpdating) {
205
      dispatch(fetchSkills());
206
    }
207
  }, [skills.length, skillsUpdating, dispatch]);
208
  return skills;
209
}
210
211
/**
212
 * Return all Experience constants from the redux store, and fetch them from backend if they are not yet in the store.
213
 * @param dispatch
214
 */
215
export function useFetchExperienceConstants(
216
  dispatch: DispatchType,
217
): {
218
  awardRecipientTypes: AwardRecipientType[];
219
  awardRecognitionTypes: AwardRecognitionType[];
220
  educationTypes: EducationType[];
221
  educationStatuses: EducationStatus[];
222
} {
223
  const awardRecipientTypes = useSelector(getAwardRecipientTypes);
224
  const awardRecipientTypesLoading = useSelector(
225
    (state: RootState) => state.awardRecipientType.loading,
226
  );
227
  useEffect(() => {
228
    if (awardRecipientTypes.length === 0 && !awardRecipientTypesLoading) {
229
      dispatch(fetchAwardRecipientTypes());
230
    }
231
  }, [awardRecipientTypes, awardRecipientTypesLoading, dispatch]);
232
233
  const awardRecognitionTypes = useSelector(getAwardRecognitionTypes);
234
  const awardRecognitionTypesLoading = useSelector(
235
    (state: RootState) => state.awardRecognitionType.loading,
236
  );
237
  useEffect(() => {
238
    if (awardRecognitionTypes.length === 0 && !awardRecognitionTypesLoading) {
239
      dispatch(fetchAwardRecognitionTypes());
240
    }
241
  }, [awardRecognitionTypes, awardRecognitionTypesLoading, dispatch]);
242
243
  const educationTypes = useSelector(getEducationTypes);
244
  const educationTypesLoading = useSelector(
245
    (state: RootState) => state.educationType.loading,
246
  );
247
  useEffect(() => {
248
    if (educationTypes.length === 0 && !educationTypesLoading) {
249
      dispatch(fetchEducationTypes());
250
    }
251
  }, [educationTypes, educationTypesLoading, dispatch]);
252
253
  const educationStatuses = useSelector(getEducationStatuses);
254
  const educationStatusesLoading = useSelector(
255
    (state: RootState) => state.educationStatus.loading,
256
  );
257
  useEffect(() => {
258
    if (educationStatuses.length === 0 && !educationStatusesLoading) {
259
      dispatch(fetchEducationStatuses());
260
    }
261
  }, [educationStatuses, educationStatusesLoading, dispatch]);
262
263
  return {
264
    awardRecipientTypes,
265
    awardRecognitionTypes,
266
    educationTypes,
267
    educationStatuses,
268
  };
269
}
270
271
export function useJobApplicationSteps(): {
272
  [step in ApplicationStep]: ProgressBarStatus;
273
} {
274
  return useSelector(getJobApplicationSteps);
275
}
276
277
/**
278
 * If the specified step of the application has status "default" (ie has not been touched yet),
279
 * then this dispatches an api request that will record the step as touched, setting it to "complete" or "error".
280
 *
281
 * NOTE: this hook only runs once, when the component is first mounted. If the true status of the step has not yet loaded
282
 * from the server, this may re-touch the step when not strictly necessary.
283
 */
284
export function useTouchApplicationStep(
285
  applicationId: number,
286
  step: ApplicationStep,
287
  dispatch: DispatchType,
288
): void {
289
  const steps = useJobApplicationSteps();
290
  useEffect(() => {
291
    if (steps[step] === "default") {
292
      dispatch(touchApplicationStep(applicationId, ApplicationStepId[step]));
293
    }
294
  }, [applicationId, step, steps, dispatch]);
295
}
296
297
/**
298
 * Return an Application (normalized, ie without Review) from the redux store, and fetch it from backend if it is not yet in the store.
299
 * @param applicationId
300
 * @param dispatch
301
 */
302
export function useFetchNormalizedApplication(
303
  applicationId: number,
304
  dispatch: DispatchType,
305
): ApplicationNormalized | null {
306
  const applicationSelector = (
307
    state: RootState,
308
  ): ApplicationNormalized | null =>
309
    getApplicationNormalized(state, { applicationId });
310
  const application: ApplicationNormalized | null = useSelector(
311
    applicationSelector,
312
  );
313
  const applicationIsUpdating = useSelector((state: RootState) =>
314
    getApplicationIsUpdating(state, { applicationId }),
315
  );
316
  const [applicationFetched, setApplicationFetched] = useState(false);
317
  useEffect(() => {
318
    if (application === null && !applicationIsUpdating && !applicationFetched) {
319
      setApplicationFetched(true);
320
      dispatch(fetchApplication(applicationId));
321
    }
322
  }, [application, applicationId, applicationIsUpdating, dispatch]);
323
  return application;
324
}
325
326
/**
327
 * Return an Application from the redux store, and fetch it from backend if it is not yet in the store.
328
 * @param applicationId
329
 * @param dispatch
330
 */
331
export function useFetchApplication(
332
  applicationId: number,
333
  dispatch: DispatchType,
334
): Application | null {
335
  const applicationSelector = (state: RootState): Application | null =>
336
    getApplicationById(state, { id: applicationId });
337
  const application: Application | null = useSelector(applicationSelector);
338
  const applicationIsUpdating = useSelector((state: RootState) =>
339
    getApplicationIsUpdating(state, { applicationId }),
340
  );
341
  useEffect(() => {
342
    if (application === null && !applicationIsUpdating) {
343
      dispatch(fetchApplication(applicationId));
344
    }
345
  }, [application, applicationId, applicationIsUpdating, dispatch]);
346
  return application;
347
}
348
349
/**
350
 * Return an Job from the redux store, and fetch it from backend if it is not yet in the store.
351
 * @param jobId
352
 * @param dispatch
353
 */
354
export function useFetchJob(
355
  jobId: number | undefined,
356
  dispatch: DispatchType,
357
): Job | null {
358
  const job = useJob(jobId);
359
  const jobUpdatingSelector = (state: RootState) =>
360
    jobId ? getJobIsUpdating(state, jobId) : false;
361
  const jobIsUpdating = useSelector(jobUpdatingSelector);
362
  useEffect(() => {
363
    // If job is null and not already updating, fetch it.
364
    if (jobId && job === null && !jobIsUpdating) {
365
      dispatch(fetchJob(jobId));
366
    }
367
  }, [jobId, job, jobIsUpdating, dispatch]);
368
  return job;
369
}
370
371
/**
372
 * Return all Experience relavant to an Application from the redux store, and fetch it from backend if it is not yet in the store.
373
 * @param applicationId
374
 * @param application
375
 * @param dispatch
376
 */
377
export function useFetchExperience(
378
  applicationId: number,
379
  application: ApplicationNormalized | null,
380
  dispatch: DispatchType,
381
): {
382
  experiences: ExperienceType[];
383
  experiencesUpdating: boolean;
384
  experiencesFetched: boolean;
385
} {
386
  const applicantId = application?.applicant_id ?? 0;
387
388
  // When an Application is still a draft, use Experiences associated with the applicant profile.
389
  // When an Application has been submitted and is no longer a draft, display Experience associated with the Application directly.
390
  const applicationLoaded = application !== null;
391
  const useProfileExperience =
392
    application === null ||
393
    application.application_status_id === ApplicationStatusId.draft;
394
395
  const experiences = useExperiences(applicationId, application);
396
  const experiencesUpdating = useSelector((state: RootState) =>
397
    useProfileExperience
398
      ? getUpdatingByApplicant(state, { applicantId })
399
      : getUpdatingByApplication(state, { applicationId }),
400
  );
401
  const [experiencesFetched, setExperiencesFetched] = useState(false);
402
  useEffect(() => {
403
    // Only load experiences if they have never been fetched by this component (!experiencesFetched),
404
    //  have never been fetched by another component (length === 0),
405
    //  and are not currently being fetched (!experiencesUpdating).
406
    // Also, wait until application has been loaded so the correct source can be determined.
407
    if (
408
      applicationLoaded &&
409
      !experiencesFetched &&
410
      !experiencesUpdating &&
411
      experiences.length === 0
412
    ) {
413
      setExperiencesFetched(true);
414
      if (useProfileExperience) {
415
        dispatch(fetchExperienceByApplicant(applicantId));
416
      } else {
417
        dispatch(fetchExperienceByApplication(applicationId));
418
      }
419
    }
420
  }, [
421
    applicantId,
422
    applicationId,
423
    applicationLoaded,
424
    dispatch,
425
    experiences.length,
426
    experiencesFetched,
427
    experiencesUpdating,
428
    useProfileExperience,
429
  ]);
430
  return {
431
    experiences,
432
    experiencesUpdating,
433
    experiencesFetched,
434
  };
435
}
436
437
/**
438
 * Return an User from the redux store, and fetch it from backend if it is not yet in the store.
439
 * @param jobId
440
 * @param dispatch
441
 */
442
export function useFetchUser(
443
  userId: number,
444
  dispatch: DispatchType,
445
): User | null {
446
  const user = useUser(userId);
447
  useEffect(() => {
448
    // If job is null and not already updating, fetch it.
449
    if (userId) {
450
      dispatch(fetchUser(userId));
451
    }
452
  }, [userId, dispatch]);
453
  return user;
454
}
455
456
/**
457
 * Trigger fetches for all data needed for the Application process which is not yet in the redux store, or in the process of loading.
458
 * @param applicationId
459
 */
460
export function useFetchAllApplicationData(
461
  applicationId: number,
462
  dispatch: DispatchType,
463
): {
464
  applicationLoaded: boolean;
465
  userLoaded: boolean;
466
  jobLoaded: boolean;
467
  criteriaLoaded: boolean;
468
  experiencesLoaded: boolean;
469
  experienceSkillsLoaded: boolean;
470
  jobQuestionsLoaded: boolean;
471
  applicationAnswersLoaded: boolean;
472
  experienceConstantsLoaded: boolean;
473
  skillsLoaded: boolean;
474
} {
475
  const application = useFetchNormalizedApplication(applicationId, dispatch);
476
  const jobId = application?.job_poster_id;
477
  const job = useFetchJob(jobId, dispatch);
478
  const { experiences, experiencesUpdating } = useFetchExperience(
479
    applicationId,
480
    application,
481
    dispatch,
482
  );
483
  const {
484
    awardRecipientTypes,
485
    awardRecognitionTypes,
486
    educationTypes,
487
    educationStatuses,
488
  } = useFetchExperienceConstants(dispatch);
489
  const skills = useFetchSkills(dispatch);
490
491
  const applicationLoaded = application !== null;
492
  const jobLoaded = job !== null;
493
  const experiencesLoaded = !experiencesUpdating || experiences.length > 0;
494
  const experienceConstantsLoaded =
495
    awardRecipientTypes.length > 0 &&
496
    awardRecognitionTypes.length > 0 &&
497
    educationTypes.length > 0 &&
498
    educationStatuses.length > 0;
499
  const skillsLoaded = skills.length > 0;
500
501
  return {
502
    applicationLoaded,
503
    jobLoaded,
504
    experiencesLoaded,
505
    experienceConstantsLoaded,
506
    skillsLoaded,
507
    criteriaLoaded: jobLoaded,
508
    experienceSkillsLoaded: experiencesLoaded,
509
    jobQuestionsLoaded: jobLoaded,
510
    applicationAnswersLoaded: applicationLoaded,
511
    userLoaded: applicationLoaded,
512
  };
513
}
514
515
/**
516
 * Trigger fetches for all data needed for the Application review process which is not yet in the redux store, or in the process of loading.
517
 * @param applicationId
518
 */
519
export function useFetchReviewApplicationData(
520
  applicantUserId: number,
521
  applicationId: number,
522
  jobId: number,
523
  dispatch: DispatchType,
524
): {
525
  applicationLoaded: boolean;
526
  jobLoaded: boolean;
527
  experiencesLoaded: boolean;
528
  experienceConstantsLoaded: boolean;
529
  skillsLoaded: boolean;
530
} {
531
  const application = useFetchApplication(applicationId, dispatch);
532
  const job = useFetchJob(jobId, dispatch);
533
  const { experiences, experiencesUpdating } = useFetchExperience(
534
    applicationId,
535
    application,
536
    dispatch,
537
  );
538
  const {
539
    awardRecipientTypes,
540
    awardRecognitionTypes,
541
    educationTypes,
542
    educationStatuses,
543
  } = useFetchExperienceConstants(dispatch);
544
  const skills = useFetchSkills(dispatch);
545
546
  return {
547
    applicationLoaded: application !== null,
548
    jobLoaded: job !== null,
549
    experiencesLoaded: !experiencesUpdating || experiences.length > 0,
550
    experienceConstantsLoaded:
551
      awardRecipientTypes.length > 0 &&
552
      awardRecognitionTypes.length > 0 &&
553
      educationTypes.length > 0 &&
554
      educationStatuses.length > 0,
555
    skillsLoaded: skills.length > 0,
556
  };
557
}
558